#include "fat32.h"
#include "pmm.h"
#include "panic.h"
#include "stdio.h"
#include "string.h"
#include "proc.h"
#include "fs.h"
#include "sdcard.h"

// bpb elements
uint32 data_sec_start = 0;
uint8 sec_per_clus = 0;
uint16 byts_per_sec = 0;
uint16 byts_per_clus = 0;
// fat control
uint32 fat_sec_start = 0;
// directory control
uint32 root_dir_clus = 0;

uint8_t (*_read_sector)(uint8_t *data_buff, uint32_t sector, uint32_t count)
		= sd_read_sector;

/* @brief: pack the function _read_sector, add error checking
 */
void read_sector(uint8_t *data_buff, uint32_t sector, uint32_t count) {
	uint32 i, j, ret = 0xff;
	uint32_t num;
	// read at most 8 sectors at once
	for (i = 0; i * 8 < count; i++) {
		ret = 0xff;
		// the rest is less than 8?
		num = (count - i * 8) < 8 ? (count - i * 8) : 8;
		// 10 times failure will cause panic
		for (j = 0; ret != 0 && j < 10; ++j) {
			ret = _read_sector((uint8_t *)((uint64)data_buff + i * SECSIZE * 8), sector + i * 8, num);
		}
		if (ret == 0xff) {
			panic("read_sector failed.");
		}
	}
}

/* @brief: init fat32 file system, init global variables
 */
void fat32_init() {
	// get BPB info
	FAT32_BPB *bpb_ptr;
	bpb_ptr = (FAT32_BPB *)kalloc(SECSIZE);
	// read the first sector of the disk, where BPB is located
	read_sector((uint8_t *)bpb_ptr, 0, 1);
	if (memcmp(bpb_ptr->BS_FilSysType, "FAT32", 5) != 0) {
		panic("fat32_init: loading FAT32 file system failed.");
	}
	puts("FAT32 file system has successfully loaded.\n");
	// byts_per_clus
	sec_per_clus = bpb_ptr->BPB_SecPerClus;
	// misaligned exception
	// byts_per_sec = (*(uint16 *)(bpb_ptr->BPB_BytsPerSec));
	memcpy(&byts_per_sec, &(bpb_ptr->BPB_BytsPerSec), 2);
	byts_per_clus = sec_per_clus * byts_per_sec;
	// fat
	fat_sec_start = bpb_ptr->BPB_RsvdSecCnt;
	// data_sec_start
	data_sec_start = bpb_ptr->BPB_RsvdSecCnt + bpb_ptr->BPB_FATSz32 * bpb_ptr->BPB_NumFATs;
	// root_dir
	root_dir_clus = bpb_ptr->BPB_RootClus;

	// ser cur_dir to root directory
	cur_dir = &file_table[ROOT_DIR_FD];
	cur_dir->first_clus_num = root_dir_clus;
	strcpy(cur_dir->fname, "/");
	cur_dir->type = DIRECTORY;
	kfree((uint64)bpb_ptr, SECSIZE);
}


/* @brief: transform all the lower case letters to upper case
 * @param str: original string
 * @param size: string size
 * @return: transformed string
 */
char *toupper(char *str, int size) {
	int i;
	char *p = str;
	for (i = 0; i < size; i++) {
		if (*p >= 'a' && *p <= 'z') {
			*p -= 32;
		}
		p++;
	}
	return str;
}

/* @brief: find the next cluster of the parameter cluster number
 * 	by FAT(file allocation table)
 * @param cur_clus: current cluster number
 */
uint32 fat_idxof(uint32 cur_clus) {
	uint32 *fat_buffer = (uint32 *)kalloc(SECSIZE);
	uint32 result;
	read_sector((uint8 *)fat_buffer, fat_sec_start + (cur_clus * FAT_ENTRY_SIZE) / byts_per_sec,  1);
	result = fat_buffer[cur_clus % (byts_per_sec / FAT_ENTRY_SIZE)];
	kfree((uint64)fat_buffer, SECSIZE);
	return result;
}

/* @brief: transform file name to short directory entry name
 * @param fname: file name string
 * @param short_name: pointer to the short directory entry name string
 * @param size: pointer to the string size
 * @bug: if there are two long file name with the same starting 6 chars,
 * 	can't correctly transform
 */
void fname2short(char *fname, char *short_name) {
	int i = 0, j;
	int length = strlen(fname);
	// copy fname to result
	strcpy(short_name, fname);
	// find position of char '.'
	while (fname[i] != '.' && i < length) {
		i++;
	}
	// fname is "."(current directory) or ".."(upper level directory)
	if (i == 0) {
		j = 2;
		goto fill_space;
	}
	// file name too long
	if (i > 8) {
		// without an extension, example: abcdefghijklmn -> ABCDEF~1
		if (i == length) {
			j = SHORT_FILENAME_SIZE;
		}
		//  with a short extension, sample1: abcdefghi.txt -> ABCDEF~1TXT
		//							sample2: abcdefghi.a   -> ABCDEF~1A
		else {
			j = SHORT_FILENAME_SIZE + (length - i) - 1;
		}
		goto long_name;
	}
	// extension too long, example: abc.longext -> ABC   ~1LON
	else if (length - 1 - i > 3) {
		j = i;
		while (j < 6) {
			short_name[i] = ' ';
			j++;
		}
		j = SHORT_FILENAME_SIZE + SHORT_EXTENSION_SIZE;
		goto long_name;
	}
	// short file name: file name less 8 bytes, extension less 3 bytes
	else {
		// without an extension, example: abcde -> ABCDE
		if (i == length) {
			j = i;
			goto fill_space;
		}
		// with an extension, example1: abcde.txt -> ABCDE   TXT
		// 					  example2: abcde.a   -> ABCDE   A
		else {
			j = i;
			while (j < 8) {
				short_name[j] = ' ';
				j++;
			}
			while (i != length) {
				short_name[j++] = fname[++i];
			}
			goto fill_space;
		}
	}

	long_name:
	short_name[6] = '~';
	short_name[7] = '1';
	short_name[8] = fname[i + 1];
	short_name[9] = fname[i + 2];
	short_name[10] = fname[i + 3];

	fill_space:
	while (j < SHORT_FILENAME_SIZE + SHORT_EXTENSION_SIZE) {
		short_name[j] = ' ';
		j++;
	}
	toupper(short_name, SHORT_FILENAME_SIZE + SHORT_EXTENSION_SIZE);
}

/* @brief: search a short_dentry from a directory
 * @param result: the wanted short dentry
 * @param dclus_num: directory cluster number
 * @param fname: file name
 * @return: 0 if successful, -1 if file not found
 */
int dir_search(short_dentry *result, uint32 dclus_num, char *fname) {
	char short_name[11];
	int dentry_num = 0;
	int dentry_per_clus = byts_per_clus / sizeof(short_dentry);
	short_dentry *p;
	// must initialize
	short_dentry *dir_buffer = NULL;
	// kfree the buffer of the last time	
	dir_buffer = (short_dentry *)kalloc(byts_per_clus);;
	p = dir_buffer;
	// read directory entries
	read_sector((uint8_t *)dir_buffer, data_sec_start + (dclus_num - 2) * sec_per_clus, sec_per_clus);
	// transform file name to short dentry name
	fname2short(fname, short_name);
	// traverse all entries
	while((*(char *)p) != '\0') {
		if ((*(char *)p) == 0xe5) {
			// this entry was deleted
			p++;
			dentry_num++;
			continue;
		}
		if (!memcmp(p->DIR_Name, short_name, (uint64)(SHORT_FILENAME_SIZE + SHORT_EXTENSION_SIZE))) {
			*result = *p;
			kfree((uint64)dir_buffer, byts_per_clus);
			return 0;
		}
		p++;
		dentry_num++;
		// this cluster had been traversed, use next cluster
		if (dentry_num == dentry_per_clus) {
			dclus_num = fat_idxof(dclus_num);
			read_sector((uint8_t *)dir_buffer, data_sec_start + (dclus_num - 2) * sec_per_clus, sec_per_clus);
			dentry_num = 0;
			p = dir_buffer;
		}
	}
	kfree((uint64)dir_buffer, byts_per_clus);
	return -1;
}

/* @brief: turn short directory entry to struct file_info
 * @param dentry: short_dentry
 * @param info_ptr: file_info pointer
 */
void dentry2file_info(short_dentry *dentry, struct file_info *info_ptr) {
	info_ptr->first_clus_num = ((dentry->DIR_FstClusHI << 16) | dentry->DIR_FstClusLO);
	info_ptr->attributes = dentry->DIR_Attr;
	info_ptr->size = dentry->DIR_FileSize;
	if ((dentry->DIR_Attr & ATTR_DIRECTORY) != 0) {
		info_ptr->type = DIRECTORY;
	} else {
		info_ptr->type = FILE;
	}
}

/* @brief: open a file
 * @param fd: the file descriptor of the file-located directory
 * @param filename: file name, can be relative or absolute
 * @param flags:
 * @param mode:
 * @return: fd of the opened file, -1 if failed
 * @bug: the same file may uses more than one struct file_info
 * 	if different process open it
 */
int fat32_openat(int fd, char *filename, int flags, int mode) {
	struct file_info *info_ptr = alloc_file_info();
	short_dentry dentry;
	uint32 dclus_num;
	int new_fd;
	int temp_fd;
	int i;
	int length;
	char temp[30];

	// find the right directory and starting data cluster number
	// absolute path， starting with /
	if (*filename == '/') {
		// delete starting '/'
		filename = &filename[1];
		dclus_num = root_dir_clus;
		//
		fd = ROOT_DIR_FD;
	}
	else if (filename[0] == '.' && filename[1] == '/') {
		filename = &filename[2];
		dclus_num = cur_dir->first_clus_num;
	}
	// relative path
	else if (fd == AT_FDCWD) {
		// current working directory
		dclus_num = cur_dir->first_clus_num;
	}
	else {
		dclus_num = cur_proc->fd_table[fd]->first_clus_num;
	}
	// find next '/' if exists
	length = strlen(filename);
	for (i = 0; filename[i] != '/' && i < length; ++i);
	// next '/' exists
	if (i < length) {
		memcpy(temp, filename, i);
		temp[i] = '\0';
		temp_fd = fat32_openat(fd, temp, flags, mode);
		new_fd = fat32_openat(temp_fd, &filename[i], flags, mode);
		fs_close(temp_fd);
		return new_fd;
	}
	// next '/' not exists, this is the last directory
	if ((dir_search(&dentry, dclus_num, filename))) {
		// file not found

		return -1;
	}
	strcpy(info_ptr->fname, filename);
	dentry2file_info(&dentry, info_ptr);
	new_fd = alloc_fd();
	cur_proc->fd_table[new_fd] = info_ptr;
	return new_fd;
}

/* @brief: load file from disk to memory
 * @param fclus_num: data's start cluster number
 * @param load_addr: load address
 * @param total_size: total size of the file
 * @return: virtual address of loaded file
 */
uint64 load_file(uint32 fclus_num, uint32 total_size) {
	// already loaded file size
	uint32 loaded = 0;
	// once loaded size, must <= byts_per_clus
	uint64 once_size;
	// next data cluster number
	uint32 next_clus = fclus_num;
	// file loaded address
	uint64 load_addr = kalloc(total_size);
	uint64 p = load_addr;
	// load cluster by cluster, max load unit is byts_per_clus
	while(next_clus != FAT_FILE_END) {
		once_size = total_size - loaded > byts_per_clus ? byts_per_clus : total_size - loaded;
		read_sector((uint8_t *)p, data_sec_start + (next_clus - 2) * sec_per_clus, ROUNDUP(once_size - 1, SECSIZE) / SECSIZE);
		p += once_size;
		loaded += once_size;
		next_clus = fat_idxof(next_clus);
	}
	return load_addr;
}

/* @brief: read from a file
 * @param fd: file fd
 * @param buf: buffer of the content to be read
 * @param size: content size
 * @return: read bytes number
 */
int fat32_read(int fd, void *buf, uint32 size) {
	uint64 load_addr;
	load_addr = load_file(cur_proc->fd_table[fd]->first_clus_num, size);
	memcpy((void *)buf, (void *)load_addr, size);
	kfree(load_addr, size);
	return size;
}

